前面介紹到動態使用 Emit IL 建立 ADO.NET Mapping 方法,但單就這功能無法讓 Dapper 被稱為輕量ORM效率之王。
因為動態建立方法是需要成本、並耗費時間
的動作,單純使用反而會拖慢速度。但當配合 Cache 後就不一樣,將建立好的方法保存在 Cache 內,可以用『空間換取時間』
概念加快查詢的效率,也就是俗稱查表法
。
接著追蹤Dapper源碼,這次需要特別關注的是QueryImpl方法下的Identity、GetCacheInfo
Identity主要封裝各緩存的比較Key屬性 :
接著搭配GetCacheInfo方法內Dapper使用的緩存類別ConcurrentDictionary<Identity, CacheInfo>
,使用TryGetValue
方法時會去先比對HashCode接著比對Equals特性,如圖片源碼。
將Key類別Identity藉由override Equals
方法實現緩存比較算法,可以看到以下Dapper實作邏輯,只要一個屬性不一樣就會建立一個新的動態方法、緩存。
public bool Equals(Identity other)
{
if (ReferenceEquals(this, other)) return true;
if (ReferenceEquals(other, null)) return false;
int typeCount;
return gridIndex == other.gridIndex
&& type == other.type
&& sql == other.sql
&& commandType == other.commandType
&& connectionStringComparer.Equals(connectionString, other.connectionString)
&& parametersType == other.parametersType
&& (typeCount = TypeCount) == other.TypeCount
&& (typeCount == 0 || TypesEqual(this, other, typeCount));
}
以此概念拿之前Emit版本修改成一個簡單Cache Demo讓讀者感受:
public class Identity
{
public string sql { get; set; }
public CommandType? commandType { get; set; }
public string connectionString { get; set; }
public Type type { get; set; }
public Type parametersType { get; set; }
public Identity(string sql, CommandType? commandType, string connectionString, Type type, Type parametersType)
{
this.sql = sql;
this.commandType = commandType;
this.connectionString = connectionString;
this.type = type;
this.parametersType = parametersType;
unchecked
{
hashCode = 17; // we *know* we are using this in a dictionary, so pre-compute this
hashCode = (hashCode * 23) + commandType.GetHashCode();
hashCode = (hashCode * 23) + (sql?.GetHashCode() ?? 0);
hashCode = (hashCode * 23) + (type?.GetHashCode() ?? 0);
hashCode = (hashCode * 23) + (connectionString == null ? 0 : StringComparer.Ordinal.GetHashCode(connectionString));
hashCode = (hashCode * 23) + (parametersType?.GetHashCode() ?? 0);
}
}
public readonly int hashCode;
public override int GetHashCode() => hashCode;
public override bool Equals(object obj) => Equals(obj as Identity);
public bool Equals(Identity other)
{
if (ReferenceEquals(this, other)) return true;
if (ReferenceEquals(other, null)) return false;
return type == other.type
&& sql == other.sql
&& commandType == other.commandType
&& StringComparer.Ordinal.Equals(connectionString, other.connectionString)
&& parametersType == other.parametersType;
}
}
public static class DemoExtension
{
private static readonly Dictionary<Identity, Func<DbDataReader, object>> readers = new Dictionary<Identity, Func<DbDataReader, object>>();
public static IEnumerable<T> Query<T>(this IDbConnection cnn, string sql,object param=null) where T : new()
{
using (var command = cnn.CreateCommand())
{
command.CommandText = sql;
using (var reader = command.ExecuteReader())
{
var identity = new Identity(command.CommandText, command.CommandType, cnn.ConnectionString, typeof(T), param?.GetType());
// 2. 如果cache有資料就使用,沒有資料就動態建立方法並保存在緩存內
if (!readers.TryGetValue(identity, out Func<DbDataReader, object> func))
{
//動態建立方法
func = GetTypeDeserializerImpl(typeof(T), reader);
readers[identity] = func;
Console.WriteLine("沒有緩存,建立動態方法放進緩存");
}else{
Console.WriteLine("使用緩存");
}
// 3. 呼叫生成的方法by reader,讀取資料回傳
while (reader.Read())
{
var result = func(reader as DbDataReader);
yield return result is T ? (T)result : default(T);
}
}
}
}
private static Func<DbDataReader, object> GetTypeDeserializerImpl(Type type, IDataReader reader, int startBound = 0, int length = -1, bool returnNullIfFirstMissing = false)
{
//..略
}
}
效果圖 :